package com.jay.compass.bt;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Formatter;
import java.util.UUID;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.IBluetooth;
import android.bluetooth.IBluetoothCallback;
import android.bluetooth.BluetoothSocket;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.ParcelUuid;
import android.os.RemoteException;
import android.util.Log;

/**
 * This class does all the work for setting up and managing Bluetooth
 * connections with other devices. It has a thread that listens for incoming
 * connections, a thread for connecting with a device, and a thread for
 * performing data transmissions when connected.
 */
public class Btcs {
	// Debugging
	private static final String TAG = "BTCS";
	private static final boolean DEBUG_ALL = false;
	private static final boolean DEBUG = DEBUG_ALL;
	private static final boolean DEBUG_WRITE = DEBUG_ALL;

	// Name for the SDP record when creating server socket
	// private static final String NAME = "Tianyuan";

	// Unique UUID for this application
	private static final UUID MY_UUID = UUID
			.fromString("00001101-0000-1000-8000-00805F9B34FB");

	// Member fields
	private final BluetoothAdapter mAdapter;
	private final Handler mHandler;
	private ConnectThread mConnectThread;
	private ConnectedThread mConnectedThread;
	private int mState;
	private int pState;

	// Constants that indicate the current connection state
	public static final int STATE_NONE = 0; // we're doing nothing
	public static final int STATE_LISTEN = 1; // now listening for incoming
												// connections
	public static final int STATE_CONNECTING = 2; // now initiating an outgoing
													// connection
	public static final int STATE_CONNECTED = 3; // now connected to a remote
													// device
	public static final int STATE_SCANNING = 4;
	public boolean force_down = false;

	/**
	 * Constructor. Prepares a new BluetoothChat session.
	 * 
	 * @param context
	 *            The UI Activity Context
	 * @param handler
	 *            A Handler to send messages back to the UI Activity
	 */
	// public Btcs(Context context, Handler handler) {
	public Btcs(Handler handler) {
		mAdapter = BluetoothAdapter.getDefaultAdapter();
		mState = STATE_NONE;
		mHandler = handler;
	}

	/**
	 * Set the current state of the chat connection
	 * 
	 * @param state
	 *            An integer defining the current connection state
	 */
	public synchronized void setState(int state) {

		if (mState == state) {
			// Do nothing
		} else if (state == -1) {
			pState = mState;
			if (isConnecting()) {
				mState = STATE_CONNECTING;
			} else if (mAdapter.isDiscovering()) {
				mState = STATE_SCANNING;
			} else if (isConnected()) {
				mState = STATE_CONNECTED;
			} else {
				mState = STATE_NONE;
			}
			state = mState;
		} else {
			pState = mState;
			mState = state;
		}
		if (DEBUG)
			Log.d(TAG, "setState() " + pState + " -> " + mState);

		// Give the new state to the Handler so the UI Activity can update
		mHandler.obtainMessage(BluetoothService.MESSAGE_STATE_CHANGE, state, -1)
				.sendToTarget();
	}

	/**
	 * Get current state.
	 */
	public synchronized int getState() {
		return mState;
	}

	/**
	 * Get previous state
	 * 
	 * @return
	 */
	public synchronized int getOldState() {
		return pState;
	}

	// /**
	// * Start the chat service. Specifically start AcceptThread to begin a
	// * session in listening (server) mode. Called by the Activity onResume()
	// */
	// public synchronized void start() {
	// if (DEBUG) Log.d(TAG, "start");
	//
	// // Cancel any thread attempting to make a connection
	// if (mConnectThread != null) {mConnectThread.cancel(); mConnectThread =
	// null;}
	//
	// // Cancel any thread currently running a connection
	// if (mConnectedThread != null) {mConnectedThread.cancel();
	// mConnectedThread = null;}
	//
	// // Start the thread to listen on a BluetoothServerSocket
	// if (mAcceptThread == null) {
	// mAcceptThread = new AcceptThread();
	// mAcceptThread.start();
	// }
	// setState(STATE_LISTEN);
	// }

	/**
	 * Start the ConnectThread to initiate a connection to a remote device.
	 * 
	 * @param device
	 *            The BluetoothDevice to connect
	 */
	public synchronized void connect(BluetoothDevice device, boolean attempt) {
		if (DEBUG)
			Log.d(TAG, "connect to: " + device);

		// Cancel any thread attempting to make a connection
		boolean tmp = force_down;
		force_down = true;
		/*
		 * if (mState == STATE_CONNECTING) { if (mConnectThread != null)
		 * {mConnectThread.cancel(); mConnectThread = null;} }
		 */

		// Cancel Scan
		/*
		 * if (mState == STATE_SCANNING) { if (mScanThread != null)
		 * {mScanThread.cancel(); mScanThread = null;} }
		 */

		// Cancel any thread currently running a connection
		if (mConnectedThread != null) {
			mConnectedThread.cancel();
			mConnectedThread = null;
		}

		// Start the thread to connect with the given device

		try {
			wait(100); // Wait for threads to close;
		} catch (InterruptedException e) {
			if (DEBUG)
				e.printStackTrace();
		}

		force_down = tmp;
		mConnectThread = new ConnectThread(device, attempt);
		mConnectThread.start();
		setState(STATE_CONNECTING);
	}

	/**
	 * Start the ConnectedThread to begin managing a Bluetooth connection
	 * 
	 * @param socket
	 *            The BluetoothSocket on which the connection was made
	 * @param device
	 *            The BluetoothDevice that has been connected
	 */
	public synchronized void connected(BluetoothSocket socket,
			BluetoothDevice device, boolean attempt) {
		if (DEBUG)
			Log.d(TAG, "connected");

		// Cancel the thread that completed the connection
		if (mConnectThread != null) {
			mConnectThread.cancel();
			mConnectThread = null;
		}

		// Cancel any thread currently running a connection
		if (mConnectedThread != null) {
			mConnectedThread.cancel();
			mConnectedThread = null;
		}

		// Cancel the accept thread because we only want to connect to one
		// device
		// if (mAcceptThread != null) {mAcceptThread.cancel(); mAcceptThread =
		// null;}
		// mAdapter.cancelDiscovery();

		// Start the thread to manage the connection and perform transmissions
		mConnectedThread = new ConnectedThread(socket, device, attempt);
		mConnectedThread.start();

		// Send the name of the connected device back to the UI Activity
		Message msg = mHandler
				.obtainMessage(BluetoothService.MESSAGE_DEVICE_NAME);
		Bundle bundle = new Bundle();
		bundle.putString(BluetoothService.DEVICE_NAME, device.getName());
		msg.setData(bundle);
		mHandler.sendMessage(msg);
		setState(STATE_CONNECTED);
	}

	/**
	 * Stop all threads
	 */
	public synchronized void stop(boolean force) {
		force_down = force;
		if (DEBUG)
			Log.d(TAG, "stop");
		if (mConnectThread != null) {
			mConnectThread.cancel();
			mConnectThread = null;
		}
		if (mConnectedThread != null) {
			mConnectedThread.cancel();
			mConnectedThread = null;
		}
		// if (mAcceptThread != null) {mAcceptThread.cancel(); mAcceptThread =
		// null;}
		// if (mScanThread != null) {mScanThread.cancel(); mScanThread = null;}
		setState(STATE_NONE);
	}

	/**
	 * Start scanning for devices if provided true, else turn scanning off
	 * Returns true if successful
	 * 
	 * @param scan
	 * @return
	 */
	public boolean scan(boolean scan) {
		if (scan) {
			if (mConnectThread == null) {
				if (DEBUG)
					Log.i("BTCS SCAN", "Start Scanning");
				return mAdapter.startDiscovery();
			} else {
				if (DEBUG)
					Log.i("BTCS SCAN", "mConnectThread != null");
				stop(false);
				return false;
			}
		} else {
			if (DEBUG)
				Log.i("BTCS", "Stop Scanning");
			return mAdapter.cancelDiscovery();
		}
	}

	/**
	 * True if connected
	 * 
	 * @return
	 */
	public boolean isConnected() {
		if (mConnectedThread != null) {
			return true;
		}
		return false;
	}

	/**
	 * True if connecting
	 * 
	 * @return
	 */
	public boolean isConnecting() {
		if (mConnectThread != null) {
			return true;
		}
		return false;
	}

	/**
	 * Write to the ConnectedThread in an unsynchronized manner
	 * 
	 * @param out
	 *            The bytes to write
	 * @see ConnectedThread#write(byte[])
	 */
	public void write(byte[] out) {
		String test = bytesToHexString(out);
		if (DEBUG_WRITE)
			Log.i("SEND MESSAGE", test);
		// Create temporary object
		ConnectedThread r;
		// Synchronize a copy of the ConnectedThread
		synchronized (this) {
			if (mState != STATE_CONNECTED)
				return;
			r = mConnectedThread;
		}
		// Perform the write unsynchronized
		r.write(out);
	}

	/**
	 * Indicate that the connection attempt failed Retry again unless connection
	 * is forced down
	 */
	private void connectionFailed(BluetoothDevice device) {
		// setState(STATE_LISTEN);
		setState(STATE_NONE);
		if (DEBUG)
			Log.i(TAG, "Connection Failed");
		if (force_down) {
			if (DEBUG)
				Log.i(TAG, "Forced Down");
			force_down = false;
		} else if (device != null) {
			if (DEBUG)
				Log.i(TAG, "Try without encryption");
			mConnectThread = new ConnectThread(device, false); // Do not retry
																// if fails
			mConnectThread.start();
			setState(STATE_CONNECTING);
		}
	}

	/**
	 * Indicate that the connection was lost retry once unless connection was
	 * forced down
	 */
	private void connectionLost(BluetoothDevice device) {
		// setState(STATE_LISTEN);
		setState(STATE_NONE);
		if (DEBUG)
			Log.i("connectionLost", "Connection Lost");
		if (force_down) {
			if (DEBUG)
				Log.i("connectionLost", "Forced Down");
			force_down = false;
		} else if (device != null) {
			if (DEBUG)
				Log.i("connectionLost", "Retry");
			connect(device, false); // Retry connection 1 time
		} else {
			if (DEBUG)
				Log.i("connectionLost", "Not retrying");
		}
	}

	/**
	 * This thread runs while attempting to make an outgoing connection with a
	 * device. It runs straight through; the connection either succeeds or
	 * fails.
	 */
	private class ConnectThread extends Thread {
		private final BluetoothSocket mmSocket;
		private final BluetoothDevice mmDevice;
		private final boolean attempt;

		/**
		 * Connect to device, provide true if retry attempt on fail
		 * 
		 * @param device
		 * @param retry
		 */
		public ConnectThread(BluetoothDevice device, boolean retry) {
			mmDevice = device;
			BluetoothSocket tmp = null;
			attempt = retry;

			try {
				BluetoothDevice hxm = BluetoothAdapter.getDefaultAdapter()
						.getRemoteDevice(device.getAddress());
				Method m;
				m = hxm.getClass().getMethod("createRfcommSocket",
						new Class[] { int.class });
				tmp = (BluetoothSocket) m.invoke(hxm, Integer.valueOf(1));
			} catch (IllegalArgumentException e) {
				if (DEBUG)
					e.printStackTrace();
			} catch (NoSuchMethodException e) {
				if (DEBUG)
					e.printStackTrace();
			} catch (IllegalAccessException e) {
				if (DEBUG)
					e.printStackTrace();
			} catch (InvocationTargetException e) {
				if (DEBUG)
					e.printStackTrace();
			}
			mmSocket = tmp;
		}

		public void run() {
			if (DEBUG)
				Log.i(TAG, "BEGIN mConnectThread");
			setName("ConnectThread");

			// Always cancel discovery because it will slow down a connection
			if (mAdapter.isDiscovering()) {
				if (!scan(false)) {
					synchronized (Btcs.this) {
						mConnectThread = null;
					}
				}
			}
			// Make a connection to the BluetoothSocket
			try {
				// This is a blocking call and will only return on a
				// successful connection or an exception
				mmSocket.connect();
			} catch (IOException e) {
				if (DEBUG)
					Log.d("An exception occurred during bluetooth socket connection",
							"NDB", e);
				if (attempt) {
					connectionFailed(mmDevice);
				} else {
					connectionFailed(null);
				}
				try {
					if (DEBUG)
						Log.e("mConnectThread", "Closing Socket");
					mmSocket.close();
				} catch (IOException e2) {
					if (DEBUG)
						Log.d("An exception occurred while attempting to close bluetooth socket",
								"NDB", e2);
				}
				return;
			} catch (Exception e) {
			}

			// Reset the ConnectThread because we're done
			synchronized (Btcs.this) {
				mConnectThread = null;
			}

			// Start the connected thread
			if (DEBUG)
				Log.i(TAG, "mConnectThread OK -> starting connected Thread");
			connected(mmSocket, mmDevice, true); // First attempt
		}

		/**
		 * Close current Bluetooth Socket
		 */
		public void cancel() {
			try {
				mmSocket.close();
			} catch (IOException e) {
				if (DEBUG)
					Log.e(TAG, "close() of connect socket failed", e);
			}
		}
	}

	/**
	 * This thread runs during a connection with a remote device. It handles all
	 * incoming and outgoing transmissions.
	 */
	private class ConnectedThread extends Thread {
		private final BluetoothSocket mmSocket;
		private final BluetoothDevice mmDevice;
		private final InputStream mmInStream;
		private final OutputStream mmOutStream;
		private final boolean attempt;

		/**
		 * Start connection on open Socket. Blocking function unless IO
		 * Exception on Socket Stream.
		 * 
		 * @param socket
		 * @param device
		 * @param retry
		 */
		public ConnectedThread(BluetoothSocket socket, BluetoothDevice device,
				boolean retry) {
			if (DEBUG)
				Log.d(TAG, "create ConnectedThread");
			attempt = retry;
			mmSocket = socket;
			mmDevice = device;
			InputStream tmpIn = null;
			OutputStream tmpOut = null;
			// Get the BluetoothSocket input and output streams
			try {
				tmpIn = socket.getInputStream();
				tmpOut = socket.getOutputStream();
			} catch (IOException e) {
				if (DEBUG)
					Log.e(TAG, "temp sockets not created", e);
			}

			mmInStream = tmpIn;
			mmOutStream = tmpOut;
		}

		public void run() {
			if (DEBUG)
				Log.i(TAG, "BEGIN mConnectedThread");
			// byte[] buffer = new byte[1024];
			int bytes = 0;

			// Keep listening to the InputStream while connected
			while (true) {
				try {
					// Read from the InputStream

					// bytes = mmInStream.read(buffer);

					if (mmInStream.available() > 0) { // FIXME Testing
						bytes = mmInStream.read();
						mHandler.obtainMessage(BluetoothService.MESSAGE_READ,
								1, -1, bytes).sendToTarget();
					}

					// Send the obtained bytes to the UI Activity

				} catch (IOException e) {
					if (DEBUG)
						Log.e(TAG, "disconnected", e);
					if (!e.toString().contains("Operation Canceled")) {
						if (attempt) {
							connectionLost(mmDevice); // retry
						} else {
							connectionLost(null); // no retry
						}
						break;
					}
					connectionLost(null);
					break;
				}
			}
		}

		/**
		 * Write to the connected OutStream.
		 * 
		 * @param buffer
		 *            The bytes to write
		 */
		public void write(byte[] buffer) {
			try {
				mmOutStream.write(buffer);

				// Share the sent message back to the UI Activity
				mHandler.obtainMessage(BluetoothService.MESSAGE_WRITE, -1, -1,
						buffer).sendToTarget();
			} catch (IOException e) {
				if (DEBUG)
					Log.e(TAG, "Exception during write", e);
			}
		}

		/**
		 * Close open Bluetooth Socket
		 */
		public void cancel() {
			try {
				mmSocket.close();
			} catch (IOException e) {
				if (DEBUG)
					Log.e(TAG, "close() of connect socket failed", e);
			}
		}
	}

	/**
	 * Convert byte[] to Hex String
	 * 
	 * @param bytes
	 * @return
	 */
	public static String bytesToHexString(byte[] bytes) {
		StringBuilder sb = new StringBuilder(bytes.length * 2);
		Formatter formatter = new Formatter(sb);
		for (byte b : bytes) {
			formatter.format("%02x", b);
			sb.append(" ");
		}
		formatter.close();
		return sb.toString();
	}

	/**
	 * Get MY_UUID
	 * 
	 * @return
	 */
	public UUID getUUID() {
		return MY_UUID;
	}

	/**
	 * Bluetooth class for hidden functions Used for SdpHelper
	 * 
	 * @return
	 */
	@SuppressWarnings({ "rawtypes", "unchecked" })
	private IBluetooth getIBluetooth() {
		IBluetooth ibt = null;

		try {
			Class c2 = Class.forName("android.os.ServiceManager");
			Method m2 = c2.getDeclaredMethod("getService", String.class);
			IBinder b = (IBinder) m2.invoke(null, "bluetooth");
			Class c3 = Class.forName("android.bluetooth.IBluetooth");
			Class[] s2 = c3.getDeclaredClasses();
			Class c = s2[0];
			Method m = c.getDeclaredMethod("asInterface", IBinder.class);
			m.setAccessible(true);
			ibt = (IBluetooth) m.invoke(null, b);
		} catch (Exception e) {
			if (DEBUG)
				Log.e("flowlab", "Erroraco!!! " + e.getMessage());
		}
		return ibt;
	}

	/**
	 * Used to call hidden function in the API. Such as Sdp discovery, channel
	 * information, etc
	 * 
	 * @author Administrator
	 */
	final class SdpHelper extends IBluetoothCallback.Stub implements
			IBluetoothCallback {
		private final IBluetooth service;
		private final ParcelUuid uuid;
		private final BluetoothDevice device;
		private int channel;
		private boolean canceled;

		public SdpHelper(BluetoothDevice device, ParcelUuid uuid) {
			service = getIBluetooth();
			this.device = device;
			this.uuid = uuid;
			canceled = false;
		}

		/**
		 * Returns the RFCOMM channel for the UUID, or throws IOException on
		 * failure.
		 */
		public synchronized int doSdp() throws IOException {
			if (canceled)
				throw new IOException("Service discovery canceled");
			channel = -1;

			boolean inProgress = false;
			try {
				inProgress = service.fetchRemoteUuids(device.getAddress(),
						uuid, this);
			} catch (RemoteException e) {
				Log.e(TAG, "", e);
			}

			if (!inProgress)
				throw new IOException("Unable to start Service Discovery");

			try {
				/*
				 * 12 second timeout as a precaution - onRfcommChannelFound
				 * should always occur before the timeout
				 */
				wait(12000); // block

			} catch (InterruptedException e) {
			}

			if (canceled)
				throw new IOException("Service discovery canceled");
			if (channel < 1)
				throw new IOException("Service discovery failed");

			return channel;
		}

		/**
		 * Object cannot be re-used after calling cancel()
		 */
		public synchronized void cancel() {
			if (!canceled) {
				canceled = true;
				channel = -1;
				notifyAll(); // unblock
			}
		}

		/**
		 * Logs channel when discovered
		 */
		public synchronized void onRfcommChannelFound(int channel) {
			if (!canceled) {
				if (DEBUG)
					Log.i("Rfcomm", "" + channel);
				// Toast.makeText(getBaseContext(),
				// getString(R.string.not_connected), Toast.LENGTH_LONG).show();
				this.channel = channel;
				notifyAll(); // unblock
			}
		}
	}
}
